2 // Edit in ODBEditor.mm
4 // Created by Allan Odgaard on 2005-11-26.
5 // See LICENSE for license details
7 // Generalized by Chris Eidhof and Eelco Lempsink from 'Edit in TextMate.mm'
9 #import <WebKit/WebKit.h>
10 #import <Carbon/Carbon.h>
12 #import "Edit in ODBEditor.h"
14 // from ODBEditorSuite.h
15 #define keyFileSender 'FSnd'
16 #define kODBEditorSuite 'R*ch'
17 #define kAEModifiedFile 'FMod'
18 #define kAEClosedFile 'FCls'
20 static NSMutableDictionary* OpenFiles;
21 static NSMutableSet* FailedFiles;
22 static NSString* ODBEditorBundleIdentifier;
23 static NSString* ODBEditorName;
25 #pragma options align=mac68k
26 struct PBX_SelectionRange
28 short unused1; // 0 (not used)
29 short lineNum; // line to select (<0 to specify range)
30 long startRange; // start of selection range (if line < 0)
31 long endRange; // end of selection range (if line < 0)
32 long unused2; // 0 (not used)
33 long theDate; // modification date/time
35 #pragma options align=reset
37 @implementation EditInODBEditor
38 + (void)setODBEventHandlers
40 NSAppleEventManager* eventManager = [NSAppleEventManager sharedAppleEventManager];
41 [eventManager setEventHandler:self andSelector:@selector(handleModifiedFileEvent:withReplyEvent:) forEventClass:kODBEditorSuite andEventID:kAEModifiedFile];
42 [eventManager setEventHandler:self andSelector:@selector(handleClosedFileEvent:withReplyEvent:) forEventClass:kODBEditorSuite andEventID:kAEClosedFile];
45 + (void)removeODBEventHandlers
47 NSAppleEventManager* eventManager = [NSAppleEventManager sharedAppleEventManager];
48 [eventManager removeEventHandlerForEventClass:kODBEditorSuite andEventID:kAEModifiedFile];
49 [eventManager removeEventHandlerForEventClass:kODBEditorSuite andEventID:kAEClosedFile];
52 + (BOOL)launchODBEditor
54 NSArray* array = [[NSWorkspace sharedWorkspace] launchedApplications];
55 for(unsigned i = [array count]; --i; )
57 if([[[array objectAtIndex:i] objectForKey:@"NSApplicationBundleIdentifier"] isEqualToString:ODBEditorBundleIdentifier])
60 return [[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:ODBEditorBundleIdentifier options:0L additionalEventParamDescriptor:nil launchIdentifier:nil];
63 + (void)asyncEditStringWithOptions:(NSDictionary*)someOptions
65 NSAutoreleasePool* pool = [NSAutoreleasePool new];
67 if(![self launchODBEditor])
72 NSData* targetBundleID = [ODBEditorBundleIdentifier dataUsingEncoding:NSUTF8StringEncoding];
73 NSAppleEventDescriptor* targetDescriptor = [NSAppleEventDescriptor descriptorWithDescriptorType:typeApplicationBundleID data:targetBundleID];
74 NSAppleEventDescriptor* appleEvent = [NSAppleEventDescriptor appleEventWithEventClass:kCoreEventClass eventID:kAEOpenDocuments targetDescriptor:targetDescriptor returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID];
75 NSAppleEventDescriptor* replyDescriptor = nil;
76 NSAppleEventDescriptor* errorDescriptor = nil;
77 AEDesc reply = { typeNull, NULL };
79 NSString* fileName = [someOptions objectForKey:@"fileName"];
80 [appleEvent setParamDescriptor:[NSAppleEventDescriptor descriptorWithDescriptorType:typeFileURL data:[[[NSURL fileURLWithPath:fileName] absoluteString] dataUsingEncoding:NSUTF8StringEncoding]] forKeyword:keyDirectObject];
82 UInt32 packageType = 0, packageCreator = 0;
83 CFBundleGetPackageInfo(CFBundleGetMainBundle(), &packageType, &packageCreator);
84 [appleEvent setParamDescriptor:[NSAppleEventDescriptor descriptorWithTypeCode:packageCreator] forKeyword:keyFileSender];
86 if(int line = [[someOptions objectForKey:@"line"] intValue])
88 PBX_SelectionRange pos = { };
90 [appleEvent setParamDescriptor:[NSAppleEventDescriptor descriptorWithDescriptorType:typeChar bytes:&pos length:sizeof(pos)] forKeyword:keyAEPosition];
93 OSStatus status = AESend([appleEvent aeDesc], &reply, kAEWaitReply, kAENormalPriority, kAEDefaultTimeout, NULL, NULL);
96 replyDescriptor = [[[NSAppleEventDescriptor alloc] initWithAEDescNoCopy:&reply] autorelease];
97 errorDescriptor = [replyDescriptor paramDescriptorForKeyword:keyErrorNumber];
98 if(errorDescriptor != nil)
99 status = [errorDescriptor int32Value];
102 NSLog(@"%s error %d", _cmd, status), NSBeep();
108 + (NSString*)extensionForURL:(NSURL*)anURL
111 if(NSString* urlString = [anURL absoluteString])
113 NSString* path = [[NSBundle bundleForClass:[self class]] pathForResource:@"url map" ofType:@"plist"];
114 NSMutableDictionary* map = [NSMutableDictionary dictionaryWithContentsOfFile:path];
116 NSString* customBindingsPath = [[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"Preferences/org.slashpunt.edit_in_odbeditor.plist"];
117 if(NSDictionary* associations = [[NSDictionary dictionaryWithContentsOfFile:customBindingsPath] objectForKey:@"URLAssociations"])
118 [map addEntriesFromDictionary:associations];
120 unsigned longestMatch = 0;
121 NSEnumerator* enumerator = [map keyEnumerator];
122 while(NSString* key = [enumerator nextObject])
124 if([urlString rangeOfString:key].location != NSNotFound && [key length] > longestMatch)
126 res = [map objectForKey:key];
127 longestMatch = [key length];
134 + (void)externalEditString:(NSString*)aString startingAtLine:(int)aLine forView:(NSView*)aView
136 [self externalEditString:aString startingAtLine:aLine forView:aView withObject:nil];
139 + (void)externalEditString:(NSString*)aString startingAtLine:(int)aLine forView:(NSView*)aView withObject:(NSObject*)anObject
141 Class cl = NSClassFromString(@"WebFrameView");
144 for(NSView* view = aView; view && !url && cl; view = [view superview])
146 if([view isKindOfClass:cl])
147 url = [[[[(WebFrameView*)view webFrame] dataSource] mainResource] URL];
150 NSString* basename = [[[[aView window] title] componentsSeparatedByString:@"/"] componentsJoinedByString:@"-"] ?: @"untitled";
151 NSString* extension = [self extensionForURL:url] ?: [[[[NSWorkspace sharedWorkspace] activeApplication] objectForKey:@"NSApplicationName"] lowercaseString];
152 NSString* fileName = [NSString stringWithFormat:@"%@/%@.%@", NSTemporaryDirectory(), basename, extension];
153 for(unsigned i = 2; [[NSFileManager defaultManager] fileExistsAtPath:fileName]; i++)
154 fileName = [NSString stringWithFormat:@"%@/%@ %u.%@", NSTemporaryDirectory(), basename, i, extension];
156 [[aString dataUsingEncoding:NSUTF8StringEncoding] writeToFile:fileName atomically:NO];
157 fileName = [fileName stringByStandardizingPath];
159 NSDictionary* options = [NSDictionary dictionaryWithObjectsAndKeys:
162 fileName, @"fileName",
163 [NSNumber numberWithInt:aLine], @"line",
164 anObject, @"object", /* last since anObject might be nil */
167 [OpenFiles setObject:options forKey:[fileName precomposedStringWithCanonicalMapping]];
168 if([OpenFiles count] == 1)
169 [self setODBEventHandlers];
170 [NSThread detachNewThreadSelector:@selector(asyncEditStringWithOptions:) toTarget:self withObject:options];
173 + (void)handleModifiedFileEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent
175 NSAppleEventDescriptor* fileURL = [[event paramDescriptorForKeyword:keyDirectObject] coerceToDescriptorType:typeFileURL];
176 NSString* urlString = [[[NSString alloc] initWithData:[fileURL data] encoding:NSUTF8StringEncoding] autorelease];
177 NSString* fileName = [[[NSURL URLWithString:urlString] path] stringByStandardizingPath];
178 NSDictionary* options = [OpenFiles objectForKey:[fileName precomposedStringWithCanonicalMapping]];
179 NSView* view = [options objectForKey:@"view"];
183 if ([view respondsToSelector:@selector(odbEditorDidModifyString:withObject:)])
185 NSString* newString = [[[NSString alloc] initWithData:[NSData dataWithContentsOfFile:fileName] encoding:NSUTF8StringEncoding] autorelease];
186 NSObject* anObject = [options objectForKey:@"object"];
187 [view performSelector:@selector(odbEditorDidModifyString:withObject:) withObject:newString withObject:anObject];
188 [FailedFiles removeObject:fileName];
191 else if([view respondsToSelector:@selector(odbEditorDidModifyString:)])
193 NSString* newString = [[[NSString alloc] initWithData:[NSData dataWithContentsOfFile:fileName] encoding:NSUTF8StringEncoding] autorelease];
194 [view performSelector:@selector(odbEditorDidModifyString:) withObject:newString];
195 [FailedFiles removeObject:fileName];
201 [FailedFiles addObject:fileName];
202 NSLog(@"%s view %p, %@, window %@", _cmd, view, view, [view window]);
203 NSLog(@"%s file name %@, options %@", _cmd, fileName, [options description]);
204 NSLog(@"%s all %@", _cmd, [OpenFiles description]);
209 + (void)handleClosedFileEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent
211 NSAppleEventDescriptor* fileURL = [[event paramDescriptorForKeyword:keyDirectObject] coerceToDescriptorType:typeFileURL];
212 NSString* urlString = [[[NSString alloc] initWithData:[fileURL data] encoding:NSUTF8StringEncoding] autorelease];
213 NSString* fileName = [[[NSURL URLWithString:urlString] path] stringByStandardizingPath];
215 if([FailedFiles containsObject:fileName])
217 if([[NSFileManager defaultManager] fileExistsAtPath:fileName])
218 [[NSWorkspace sharedWorkspace] selectFile:fileName inFileViewerRootedAtPath:[fileName stringByDeletingLastPathComponent]];
219 [FailedFiles removeObject:fileName];
223 [[NSFileManager defaultManager] removeFileAtPath:fileName handler:nil];
224 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
227 [OpenFiles removeObjectForKey:[fileName precomposedStringWithCanonicalMapping]];
228 if([OpenFiles count] == 0)
229 [self removeODBEventHandlers];
232 + (NSMenu*)findEditMenu
234 NSMenu* mainMenu = [NSApp mainMenu];
235 std::map<size_t, NSMenu*> ranked;
236 for(int i = 0; i != [mainMenu numberOfItems]; i++)
238 NSMenu* candidate = [[mainMenu itemAtIndex:i] submenu];
239 static SEL const actions[] = { @selector(undo:), @selector(redo:), @selector(cut:), @selector(copy:), @selector(paste:), @selector(delete:), @selector(selectAll:) };
241 for(int j = 0; j != sizeof(actions)/sizeof(actions[0]); j++)
243 if(-1 != [candidate indexOfItemWithTarget:nil andAction:actions[j]])
247 if(score > 0 && ranked.find(score) == ranked.end())
248 ranked[score] = candidate;
250 return ranked.empty() ? nil : (--ranked.end())->second;
253 + (void)installMenuItem:(id)sender
255 if(NSMenu* editMenu = [self findEditMenu])
257 [editMenu addItem:[NSMenuItem separatorItem]];
258 NSString* ellips = [NSString stringWithUTF8String:"\xe2\x80\xa6"]; // utf-8 for the '...' character (literal utf8 is not allowed in source code)
259 NSMenuItem *menuItem = [editMenu addItemWithTitle:[NSString stringWithFormat:@"Edit in %@%@", ODBEditorName, ellips] action:@selector(editInODBEditor:) keyEquivalent:@"E"];
260 [menuItem setKeyEquivalentModifierMask:NSControlKeyMask | NSCommandKeyMask];
266 OpenFiles = [NSMutableDictionary new];
267 FailedFiles = [NSMutableSet new];
268 NSString* mainBundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; // reads app we're used inside off
269 NSString* bundleIdentifier = @"org.slashpunt.edit_in_odbeditor"; // XXX Should this be hardcoded?
270 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
271 [defaults addSuiteNamed:bundleIdentifier];
272 NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
273 @"NO", @"DisableEditInODBEditorMenuItem",
274 @"", @"ODBEditorBundleIdentifier",
275 @"<Unknown>", @"ODBEditorName",
278 [defaults registerDefaults:appDefaults];
280 ODBEditorBundleIdentifier = [[defaults stringForKey:@"ODBEditorBundleIdentifier"] retain] ?: @"";
281 ODBEditorName = [[defaults stringForKey:@"ODBEditorName"] retain] ?: @"<Unknown>";
282 if([defaults boolForKey:@"DisableEditInODBEditorMenuItem"] == NO
283 && ![ODBEditorBundleIdentifier isEqualToString:@""]
284 && ![ODBEditorBundleIdentifier isEqualToString:mainBundleIdentifier])
285 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(installMenuItem:) name:NSApplicationDidFinishLaunchingNotification object:[NSApplication sharedApplication]];